package oleg.match; 

/**
 * Copyright 2008 Oleg Frenkel
 *
 * This file is part of PerfectSearch.
 *
 * PerfectSearch is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * PerfectSearch is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with PerfectSearch.  If not, see <http://www.gnu.org/licenses/>.
 */

import java.util.*;
import javax.microedition.pim.Contact;

import oleg.*;
import oleg.cfg.Config;

/**
 * This class provides cache for finding out if specific contact matches input text
 * for prediction mode. This includes both "start with text" and "contains text".
 */

public class Matcher1 implements IMatcher
{
  private ContactUtils contUtils = new ContactUtils();
//   private Hashtable acceptedMap = new Hashtable();
  private char[][] acceptedCharsByNumChar = new char[][] {
    {}, //0
    {}, //1
    {'A', 'B', 'C'}, //2
    {'D', 'E', 'F'}, //3
    {'G', 'H', 'I'}, //4
    {'J', 'K', 'L'}, //5
    {'M', 'N', 'O'}, //6
    {'P', 'Q', 'R', 'S'}, //7
    {'T', 'U', 'V'}, //8
    {'W', 'X', 'Y', 'Z'}}; //9

  // cache
  private static final String NOT_FOUND = "NOT_FOUND_18273645437392746545378292846465353";
  private static final String SEPARATOR = "_^%87_";
  private Hashtable startPredCache = new Hashtable(100);
  private Hashtable containPredCache = new Hashtable(100);

  private Vector allContacts;
  private Vector matchingStartNameContacts = new Vector();
  private Vector matchingContacts = new Vector();
//   private String inputText = "";
//   private int padMode = -1;
  
  public Matcher1(Vector allContacts) {
    setAllContacts(allContacts);
    // acceptedMap is not used as it is replaced with hopefully more 
    // efficient code
//     Vector v2 = new Vector();
//     v2.addElement(new Character('A'));
//     v2.addElement(new Character('B'));
//     v2.addElement(new Character('C'));
//     acceptedMap.put(new Character('2'), v2);

//     Vector v3 = new Vector();
//     v3.addElement(new Character('D'));
//     v3.addElement(new Character('E'));
//     v3.addElement(new Character('F'));
//     acceptedMap.put(new Character('3'), v3);

//     Vector v4 = new Vector();
//     v4.addElement(new Character('G'));
//     v4.addElement(new Character('H'));
//     v4.addElement(new Character('I'));
//     acceptedMap.put(new Character('4'), v4);

//     Vector v5 = new Vector();
//     v5.addElement(new Character('J'));
//     v5.addElement(new Character('K'));
//     v5.addElement(new Character('L'));
//     acceptedMap.put(new Character('5'), v5);

//     Vector v6 = new Vector();
//     v6.addElement(new Character('M'));
//     v6.addElement(new Character('N'));
//     v6.addElement(new Character('O'));
//     acceptedMap.put(new Character('6'), v6);

//     Vector v7 = new Vector();
//     v7.addElement(new Character('P'));
//     v7.addElement(new Character('Q'));
//     v7.addElement(new Character('R'));
//     v7.addElement(new Character('S'));
//     acceptedMap.put(new Character('7'), v7);

//     Vector v8 = new Vector();
//     v8.addElement(new Character('T'));
//     v8.addElement(new Character('U'));
//     v8.addElement(new Character('V'));
//     acceptedMap.put(new Character('8'), v8);

//     Vector v9 = new Vector();
//     v9.addElement(new Character('W'));
//     v9.addElement(new Character('X'));
//     v9.addElement(new Character('Y'));
//     v9.addElement(new Character('Z'));
//     acceptedMap.put(new Character('9'), v9);
  }

  public void setAllContacts(Vector allContacts) {
    this.allContacts = allContacts;
    clearCache();
    matchingStartNameContacts.removeAllElements();
    matchingContacts.removeAllElements();
  }

  public void clearCache() {
    throw new RuntimeException("Matcher1: clearCache is not implemented");
  }


  public void updateMatchingContacts(String inputText) {
    matchingStartNameContacts.removeAllElements();
    matchingContacts.removeAllElements();

    for (int i = 0; i < allContacts.size(); i++) {
      Contact curCont = (Contact)allContacts.elementAt(i);

      switch (matchContact(curCont, inputText)) {
      case C.START_MATCH:
        matchingStartNameContacts.addElement(curCont);
        break;
      case C.CONTAIN_MATCH:
        matchingContacts.addElement(curCont);
        break;
      }
    }
  }

  public Vector getMatchingStartNameContacts() {
    return matchingStartNameContacts;
  }

  public Vector getMatchingContacts() {
    return matchingContacts;
  }

  public String getDisplayText(Contact curCont, String inputText) {
    switch (Config.instance().getPadMode()) {
    case C.PAD_MODE_PRED_OFF:
    case C.PAD_MODE_NUM:
      return inputText;
    case C.PAD_MODE_PRED_ON:
      if (curCont == null) {
        return inputText;
      }

      String startsStr = startsWithPred(curCont, Contact.NAME, inputText);
      if (startsStr != null) {
        return startsStr;
      } else {
        int[] allFields = curCont.getPIMList().getSupportedFields();
        for (int j = 0; j < allFields.length; j++) {
          int curField = allFields[j];
          String containsStr = containsPred(curCont, curField, inputText);
          if (containsStr != null) {
            return containsStr;
          }
        }
      }

      return inputText;
    }

    throw new RuntimeException("Unknown pad mode: " + Config.instance().getPadMode());
  }

  private int matchContact(Contact curCont, String inputText) {
    switch (Config.instance().getPadMode()) {
    case C.PAD_MODE_PRED_OFF:
    case C.PAD_MODE_NUM:
      // First find if any name field starts with specified text
      if (startsWithStr(curCont, Contact.NAME, inputText)) {
        return C.START_MATCH;
      } else {
        int[] allFields = curCont.getPIMList().getSupportedFields();
        for (int j = 0; j < allFields.length; j++) {
          int curField = allFields[j];
          if (containsStr(curCont, curField, inputText)) {
            return C.CONTAIN_MATCH;
          }
        }
      }

      return C.NO_MATCH;
    case C.PAD_MODE_PRED_ON:
      if (curCont == null) {
        return C.NO_MATCH;
      }

      if (startsWithPred(curCont, Contact.NAME, inputText) != null) {
        return C.START_MATCH;
      } else {
        int[] allFields = curCont.getPIMList().getSupportedFields();
        for (int j = 0; j < allFields.length; j++) {
          int curField = allFields[j];
          if (containsPred(curCont, curField, inputText) != null) {
            return C.CONTAIN_MATCH;
          }
        }
      }

      return C.NO_MATCH;
    }

    throw new RuntimeException("Unknown pad mode: " + Config.instance().getPadMode());
  }

  // Returns string that matches prediction, or null if no prediction exists
  private String startsWithPred(Contact curCont, int field, String inputText) {
    Vector values = contUtils.getValuesAsVector(curCont, field);
    String cacheKey = getCacheKey(values, inputText);
    String res = (String)startPredCache.get(cacheKey);
    if (res == null) {
      for (int i = 0; i < values.size(); i++) {
        String curValue = (String)values.elementAt(i);
        if (startsWithPred(curValue, inputText)) {
          res = curValue.substring(0, inputText.length());
        }
      }

      if (res == null) {
        res = NOT_FOUND;
      }

      startPredCache.put(cacheKey, res);
    }

    return res == NOT_FOUND ? null : res;
  }

  // Returns string that matches prediction, or null if no prediction exists
  private String containsPred(Contact curCont, int field, String inputText) {
    Vector values = contUtils.getValuesAsVector(curCont, field);
    String cacheKey = getCacheKey(values, inputText);
    String res = (String)containPredCache.get(cacheKey);
    if (res == null) {
      for (int i = 0; i < values.size(); i++) {
        String curValue = (String)values.elementAt(i);
        for (int j = 0; j <= curValue.length() - inputText.length(); j++) {
          String curSubValue = curValue.substring(j);
          if (startsWithPred(curSubValue, inputText)) {
            res = curSubValue.substring(0, inputText.length());
          }
        }
      }

      if (res == null) {
        res = NOT_FOUND;
      }

      containPredCache.put(cacheKey, res);
    }

    return res == NOT_FOUND ? null : res;
  }

  private boolean startsWithPred(String curValueOrig, String inputText) {
    String curValue = curValueOrig.toUpperCase();
      
    // Performance improvement: create array of Character for curValue and use
    // it rather than "new Character(curValue.charAt(j)" below.

    if (inputText.length() > curValue.length()) {
      // Entered input text is longer than current value, so current value cannot
      // start with entered input text.
      return false;
    }

    // The code below is replaced with hopefully more efficient code
//     for (int j = 0; j < inputText.length(); j++) {
//       Character c = new Character(inputText.charAt(j));
//       Vector acceptedStrings = (Vector)acceptedMap.get(c);
//       if (!acceptedStrings.contains(new Character(curValue.charAt(j)))) {
//         return false;
//       }
//     }

    for (int j = 0; j < inputText.length(); j++) {
      char c = inputText.charAt(j);
      int index = 0;
      switch (c) {
      case '0':
        index = 0;
      case '1':
        index = 1;
        break;
      case '2':
        index = 2;
        break;
      case '3':
        index = 3;
        break;
      case '4':
        index = 4;
        break;
      case '5':
        index = 5;
        break;
      case '6':
        index = 6;
        break;
      case '7':
        index = 7;
        break;
      case '8':
        index = 8;
        break;
      case '9':
        index = 9;
        break;
      }
      char[] acceptedChars = acceptedCharsByNumChar[index];
      boolean matches = false;
      for (int k = 0; k < acceptedChars.length; k++) {
        if (acceptedChars[k] == curValue.charAt(j)) {
          matches = true;
          break;
        }
      }

      if (!matches) {
        return false;
      }
    }

    return true;
  }

  private boolean startsWithStr(Contact curCont, int field, String inputText) {
    Vector values = contUtils.getValuesAsVector(curCont, field);
    for (int i = 0; i < values.size(); i++) {
      String curValue = (String)values.elementAt(i);
      if (curValue.toUpperCase().startsWith(inputText)) {
        return true;
      }
    }

    return false;
  }

  private boolean containsStr(Contact curCont, int field, String inputText) {
    Vector values = contUtils.getValuesAsVector(curCont, field);
    for (int i = 0; i < values.size(); i++) {
      String curValue = (String)values.elementAt(i);
      if (curValue.toUpperCase().indexOf(inputText) >= 0) {
        return true;
      }
    }

    return false;
  }

  // Cache functions
  private String getCacheKey(Vector values, String str) {
    StringBuffer sb = new StringBuffer(str + SEPARATOR);
    for (int i = 0; i < values.size(); i++) {
      sb.append((String)values.elementAt(i) + SEPARATOR);
    }

    return sb.toString();
  }

}